import json
import random
import re
from django.conf import settings
from django import http
from django.contrib.auth import login, authenticate, logout
from django.http import HttpResponseForbidden
from django.http import JsonResponse
from django.shortcuts import render, redirect
from django.urls import reverse
from django.views import View
from django_redis import get_redis_connection
from pymysql import DatabaseError

from apps.areas.models import Address
from apps.carts.utils import merge_cart_cookie_to_redis
from apps.goods import models
from apps.goods.models import SKU
from utils.cookiesecret import CookieSecret
from . import constants
from itsdangerous import BadData
from apps.users.models import User
from apps.users.utils import generate_verify_email_url, human_or_machine
from apps.verifications.constants import MACHINE_TIMES, MACHINE_TIMES_WAIT
from utils.response_code import RETCODE
from django.contrib.auth.mixins import LoginRequiredMixin
from utils.views import LoginRequiredJSONMixin


# Create your views here.


# 13. 项目实训1：忘记密码，第三步，修改用户密码
class FindPasswordNewPassword(View):
    def post(self, request, mobile):
        # 接收参数
        json_dict = request.body
        try:
            json_dict = str(json_dict.decode())
            if not re.match(r'^[:}{=\w",]+$', json_dict):
                return JsonResponse({"status": 5001, 'message': "验证信息错误!"})
            json_dict = json.loads(json_dict)
        except Exception:
            return JsonResponse({"status": 5001, 'message': "验证信息错误!"})
        access_token = json_dict.get('access_token')
        password = json_dict.get('password')
        password2 = json_dict.get('password2')
        if not all([access_token, password, password2]):
            return JsonResponse({"status": 5003, 'message': "参数不全!"})

        # 参数正则校验
        if not re.match('^\w+=$', access_token):
            return JsonResponse({"status": 5001, 'message': "验证信息错误!"})
        if not re.match(r'^[0-9A-Za-z]{8,20}$', password):
            return http.HttpResponseForbidden('请输入8-20位的密码，可使用数字、字母')
        if not re.match(r'^[0-9A-Za-z]{8,20}$', password2):
            return http.HttpResponseForbidden('请输入8-20位的密码，可使用数字、字母')
        try:
            access_token = str(CookieSecret.loads(access_token))
        except Exception as e:
            settings.LOGGER.warning(e)
            return JsonResponse({"status": 5003, 'message': "验证信息错误!"})
        if not re.match(r'^\d+$',access_token):
            return JsonResponse({"status": 5003, 'message': "验证信息错误!"})
        # 校验两次密码是否一致
        if not password==password2:
            return JsonResponse({"status": 5004, 'message': "两次密码不一致!"})
        # 校验TOKEN值
        try:
            access_client = get_redis_connection("default")
        except Exception as e:
            settings.LOGGER.error(e)
            return JsonResponse({"status": 5003, 'message': "验证失败..."})
        client_token = access_client.get('t2_mobile_' + str(mobile))
        if client_token:
            try:
                client_token = client_token.decode()
            except Exception as e:
                settings.LOGGER.error(e)
                return JsonResponse({"status": 5003, 'message': "验证信息错误！"})
        else:
            return JsonResponse({"status": 5003, 'message': "验证信息已过期"})

        if not client_token == access_token:
            return JsonResponse({"status": 5003, 'message': "验证信息错误!"})
        # 记录新密码入库（注意使用user.set_password,并且save）
        try:
            user = User.objects.get(mobile=mobile)
        except Exception:
            return JsonResponse({"status": 5002, 'message': "不存在的用户"})
        user.set_password(password)
        user.save()
        try:
            access_client.delete('t2_mobile_' + str(mobile))
        except Exception as e:
            settings.LOGGER.error(e)
        # 返回响应对象
        return JsonResponse({"status": 5000, 'message': "修改成功"})


# 12. 项目实训1：忘记密码，第2点2步，校验短信验证码
class FindPasswordCheckSmsCodes(View):
    def get(self, request, mobile):
        # 校验参数格式
        sms_code = request.GET.get('sms_code')
        try:
            if not re.match(r'^\d{6}$', sms_code):
                return JsonResponse({"status": 5001, 'message': "短信验证码错误"})
        except Exception as e:
            settings.LOGGER.warning(e)
            return JsonResponse({"status": 5001, 'message': "短信验证码错误"})
        # 链接数据库提取验证码内容
        try:
            sms_redis_client = get_redis_connection('sms_code')
        except Exception:
            return JsonResponse({"status": 5002, 'message': "验证失败"})
        client_sms_code = sms_redis_client.get("sms_%s" % mobile)
        if not client_sms_code:
            return JsonResponse({"status": 5001, 'message': "短信验证码已过期，请重新获取"})
        # 判断两者是否相等
        client_sms_code = str(client_sms_code.decode())  # 解码
        if not client_sms_code == sms_code:
            return JsonResponse({"status": 5001, 'message': "短信验证码错误"})
        else:
            # 两者相等，可以进行下一步
            try:
                sms_redis_client.delete("sms_%s" % mobile)  # 删除已经验证成功的验证码
            except:
                pass
            access_token = ''
            for i in range(constants.TOKEN_LENGTH):
                access_token += str(random.randint(0, 9))
            # 没有加签加盐，如果这样做，就需要限流，防止被解密
            access_client = get_redis_connection("default")
            access_client.setex('t2_mobile_' + str(mobile), constants.USER_TOKEN_COUNTS, access_token)
            access_token = CookieSecret.dumps(access_token)
            try:
                access_client.delete('t_mobile_' + str(mobile))
            except:
                # 删不掉很正常，完全可能已经过期了
                pass
            return JsonResponse({"status": 5000, "user_id": mobile, "access_token": access_token})


# 12. 项目实训1：忘记密码，第2点1步，发送短信验证码
class FindPasswordSmsCodes(View):
    def get(self, request, mobile):
        # 获取token
        access_token = request.GET.get('access_token')
        try:
            if not re.match('^\w+=$', access_token):
                return JsonResponse({"status": 5001, 'message': "验证信息错误!"})
        except Exception as e:
            settings.LOGGER.warning(e)
            return JsonResponse({"status": 5001, 'message': "验证信息错误!"})
        # 获取mobile对应的后台token
        access_client = get_redis_connection("default")
        client_token = access_client.get('t_mobile_' + mobile)
        if not client_token:
            return JsonResponse({"status": 5001, 'message': "验证信息已过期或手机号参数错误!"})
        # 解码解密
        client_token = client_token.decode()
        client_token = str(client_token)
        try:
            access_token = CookieSecret.loads(access_token)
            access_token = str(access_token)
        except Exception:
            return JsonResponse({"status": 5001, 'message': "验证信息错误!"})
        # 正则校验验证信息
        from . import constants
        if access_client.get('t_mobile_stop_' + mobile):
            return JsonResponse({"status": 5001, 'message': "请稍后再试"})
        if not re.match(r'^\d+$', access_token):
            access_client.setex('t_mobile_stop_' + mobile, constants.USER_TOKEN_COUNTS, 1)
            return JsonResponse({"status": 5001, 'message': "验证信息错误，请%s秒后重试！" % constants.USER_TOKEN_COUNTS})
        # 对比信息
        if not client_token == access_token:
            access_client.setex('t_mobile_stop_' + mobile, constants.USER_TOKEN_COUNTS, 1)
            return JsonResponse({"status": 5001, 'message': "验证信息错误，请%s秒后重试！" % constants.USER_TOKEN_COUNTS})

        # 生成短信验证码,时间等设置保持与Verfication一致
        from random import randint
        sms_code = "%06d" % randint(0, 999999)
        sms_redis_client = get_redis_connection('sms_code')
        from apps.verifications import constants
        sms_redis_client.setex("sms_%s" % mobile, constants.SMS_CODE_REDIS_EXPIRES, sms_code)
        print("当前验证码是:", sms_code)
        # 发送短信
        redis_conn = get_redis_connection('send_flag_code')
        send_flag = redis_conn.get('send_flag_%s' % mobile)
        if send_flag:
            return JsonResponse({"status": 5001, 'message': "短信发送过于频繁，请稍后再试"})
        redis_conn.setex('send_flag_%s' % mobile, constants.SEND_SMS_CODE_INTERVAL, 1)
        # 发送短信
        from celery_tasks.sms.tasks import ccp_send_sms_code
        ccp_send_sms_code.delay(mobile, sms_code)

        return JsonResponse({"status": 5000, 'message': "短信已发送，请注意查收"})


# 11. 项目实训1：忘记密码，第一步，校验用户名参数
class FindPasswordStepOne(View):
    def get(self, request, mobile):
        # 这里使用用户名，返回用户明文电话号码显然是不合适的
        # 但修改为密文可能引发前端错误，所以直接只允许手机号输入
        image_code = request.GET.get('image_code')
        image_code_id = request.GET.get('image_code_id')

        if not all([mobile, image_code, image_code_id]):
            return
        if not re.match(r"^\w{4}$", image_code):
            return HttpResponseForbidden('参数错误，拒绝访问')
        if not re.match(r"^[\w-]{36}$", image_code_id):
            return HttpResponseForbidden('参数错误，拒绝访问')

        try:
            image_code_client = get_redis_connection('verify_image_code')
        except Exception:
            return http.HttpResponseForbidden('连接失败...')

        try:
            redis_code = image_code_client.get("img_%s" % image_code_id)
            if not redis_code:
                return JsonResponse({"status": 5001, "mobile": None, "access_token": None})
        except DatabaseError:
            return JsonResponse({"status": 5001, "mobile": None, "access_token": None})
        # 验证码不正确
        if image_code.upper() != redis_code.decode().upper():
            return JsonResponse({"status": 5001, "mobile": None, "access_token": None})

        # 删除已经正确验证的验证码
        try:
            image_code_client.delete("img_%s" % image_code_id)
        except Exception as e:
            settings.LOGGER.erro(e)
        # 把手机号验证放在这里，从而减少数据库交互
        try:
            user = User.objects.get(mobile=mobile)
        except Exception:
            # 手机号不存在
            return JsonResponse({"status": 5004, "mobile": None, "access_token": None})

        access_token = ''
        for i in range(constants.TOKEN_LENGTH):
            access_token += str(random.randint(0, 9))

        # 没有加签加盐，如果这样做，就需要限流，防止被解密
        access_client = get_redis_connection("default")
        access_client.setex('t_mobile_' + str(user.mobile), constants.USER_TOKEN_COUNTS, access_token)
        access_token = CookieSecret.dumps(access_token)
        return JsonResponse({"status": 5000, "mobile": user.mobile, "access_token": access_token})


# 10. 项目实训1：忘记密码,显示找回密码页面
class UserFindPassword(View):
    def get(self, request):
        return render(request, 'find_password.html')


# 9. 保存和查询浏览记录
class UserBrowseHistory(LoginRequiredMixin, View):
    """用户浏览记录"""

    # 这里有可能出现恶意灌数据的情况,但可以查到用户
    def post(self, request):
        """保存用户浏览记录"""
        # 1.接收json参数
        sku_id = json.loads(request.body.decode()).get('sku_id')

        # 2.根据sku_id 查询sku
        try:
            sku = SKU.objects.get(id=sku_id)
        except SKU.DoesNotExist:
            return HttpResponseForbidden('商品不存在!')

        # 3.如果有sku,保存到redis
        history_redis_client = get_redis_connection('history')
        history_key = 'history_%s' % request.user.id

        redis_pipeline = history_redis_client.pipeline()
        # 3.1 去重
        history_redis_client.lrem(history_key, 0, sku_id)
        # 3.2 存储
        history_redis_client.lpush(history_key, sku_id)
        # 3.3 截取 5个
        history_redis_client.ltrim(history_key, 0, 4)
        redis_pipeline.execute()

        # 响应结果
        return JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK'})

    def get(self, request):
        """获取用户浏览记录"""
        # 获取Redis存储的sku_id列表信息
        redis_conn = get_redis_connection('history')
        sku_ids = redis_conn.lrange('history_%s' % request.user.id, 0, -1)

        # 根据sku_ids列表数据，查询出商品sku信息
        skus = []
        for sku_id in sku_ids:
            sku = models.SKU.objects.get(id=sku_id)
            skus.append({
                'id': sku.id,
                'name': sku.name,
                'default_image_url': sku.default_image.url,
                'price': sku.price
            })

        return http.JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'skus': skus})


# 8. 增加收货地址
class CreateAddressView(LoginRequiredMixin, View):
    """新增地址"""

    def post(self, request):
        """实现新增地址逻辑"""
        # 判断是否超过地址上限：最多20个
        # Address.objects.filter(user=request.user).count()
        count = request.user.addresses.count()
        if count >= constants.USER_ADDRESS_COUNTS_LIMIT:
            return http.JsonResponse({'code': RETCODE.THROTTLINGERR, 'errmsg': '超过地址数量上限'})

        # 接收参数
        json_dict = json.loads(request.body.decode())
        receiver = json_dict.get('receiver')
        province_id = json_dict.get('province_id')
        city_id = json_dict.get('city_id')
        district_id = json_dict.get('district_id')
        place = json_dict.get('place')
        mobile = json_dict.get('mobile')
        tel = json_dict.get('tel')
        email = json_dict.get('email')

        # 校验参数
        if not all([receiver, province_id, city_id, district_id, place, mobile]):
            return http.HttpResponseForbidden('缺少必传参数')
        if not re.match(r'^1[3-9]\d{9}$', mobile):
            return http.HttpResponseForbidden('参数mobile有误')
        if tel:
            if not re.match(r'^(0[0-9]{2,3}-)?([2-9][0-9]{6,7})+(-[0-9]{1,4})?$', tel):
                return http.HttpResponseForbidden('参数tel有误')
        if email:
            if not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
                return http.HttpResponseForbidden('参数email有误')

        # 保存地址信息
        try:
            address = Address.objects.create(
                user=request.user,
                title=receiver,
                receiver=receiver,
                province_id=province_id,
                city_id=city_id,
                district_id=district_id,
                place=place,
                mobile=mobile,
                tel=tel,
                email=email
            )

            # 设置默认地址
            if not request.user.default_address:
                request.user.default_address = address
                request.user.save()
        except Exception as e:
            settings.LOGGER.error(e)
            return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '新增地址失败'})

        # 新增地址成功，将新增的地址响应给前端实现局部刷新
        address_dict = {
            "id": address.id,
            "title": address.title,
            "receiver": address.receiver,
            "province": address.province.name,
            "city": address.city.name,
            "district": address.district.name,
            "place": address.place,
            "mobile": address.mobile,
            "tel": address.tel,
            "email": address.email
        }

        # 响应保存结果
        return http.JsonResponse({'code': RETCODE.OK, 'errmsg': '新增地址成功', 'address': address_dict})


# 7. 用户收货地址定义
class AddressView(LoginRequiredMixin, View):
    """用户收货地址"""

    def get(self, request):
        """提供收货地址界面"""
        # 获取用户地址列表
        login_user = request.user
        addresses = Address.objects.filter(user=login_user, is_deleted=False)

        address_dict_list = []
        for address in addresses:
            address_dict = {
                "id": address.id,
                "title": address.title,
                "receiver": address.receiver,
                "province": address.province.name,
                "city": address.city.name,
                "district": address.district.name,
                "place": address.place,
                "mobile": address.mobile,
                "tel": address.tel,
                "email": address.email
            }
            address_dict_list.append(address_dict)

        context = {
            'default_address_id': login_user.default_address_id,
            'addresses': address_dict_list,
        }

        return render(request, 'user_center_site.html', context)


# 6.用户验证
def check_verify_email_token(token):
    """
    验证token并提取user
    :param token: 用户信息签名后的结果
    :return: user, None
    """
    from utils.secret import SecretOauth
    try:
        token_dict = SecretOauth().loads(token)
    except BadData:
        return None

    try:
        user = User.objects.get(id=token_dict['user_id'], email=token_dict['email'])
    except Exception as e:
        settings.LOGGER.error(e)
        return None
    else:
        return user


class VerifyEmailView(View):
    """验证邮箱"""

    def get(self, request):
        """实现邮箱验证逻辑"""
        # 接收参数
        token = request.GET.get('token')

        # 校验参数：判断token是否为空和过期，提取user
        if not token:
            return http.HttpResponseBadRequest('缺少token')

        user = check_verify_email_token(token)
        if not user:
            return http.HttpResponseForbidden('无效的token')

        # 修改email_active的值为True
        try:
            user.email_active = True
            user.save()
        except Exception as e:
            settings.LOGGER.error(e)
            return http.HttpResponseServerError('激活邮件失败')

        # 返回邮箱验证结果
        return redirect(reverse('users:info'))


# 5.添加邮箱
class EmailView(LoginRequiredJSONMixin, View):
    """添加邮箱"""

    def put(self, request):
        """实现添加邮箱逻辑"""
        # 接收参数
        json_str = request.body.decode()
        json_dict = json.loads(json_str)
        email = json_dict.get('email')

        # 校验参数
        if not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
            return http.HttpResponseForbidden('参数email有误')

        try:
            request.user.email = email
            request.user.save()
        except Exception as e:
            settings.LOGGER.error(e)
            return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '添加邮箱失败'})

        # 4.异步发送邮件
        verify_url = generate_verify_email_url(request.user)
        from celery_tasks.email.tasks import send_verify_email
        send_verify_email.delay(email, verify_url)

        # 响应添加邮箱结果
        return http.JsonResponse({'code': RETCODE.OK, 'errmsg': '添加邮箱成功'})


# 4.用户中心
class UserInfoView(LoginRequiredMixin, View):
    """用户中心"""

    def get(self, request):
        """提供个人信息界面"""
        context = {
            'username': request.user.username,
            'mobile': request.user.mobile,
            'email': request.user.email,
            'email_active': request.user.email_active
        }
        return render(request, 'user_center_info.html', context=context)


# 3.login
class LogoutView(View):
    """退出登录"""

    def get(self, request):
        """实现退出登录逻辑"""
        # 清理session
        logout(request)
        # 退出登录，重定向到登录页
        response = redirect(reverse('contents:index'))
        # 退出登录时清除cookie中的username
        response.delete_cookie('username')

        return response


# 2.用户登陆功能
class LoginView(View):
    """用户名登录"""

    def get(self, request):
        """        提供登录界面        """
        return render(request, 'login.html')

    def post(self, request):
        """        实现登录逻辑        """
        # 1.接收三个参数
        username = request.POST.get('username')
        password = request.POST.get('password')
        remembered = request.POST.get('remembered')

        # 2.校验参数
        if not all([username, password, remembered]):
            return HttpResponseForbidden('参数不齐全')
        # 2.1 用户名
        if not re.match(r'^\w{5,20}$', username):
            return HttpResponseForbidden('请输入5-20个字符的用户名或手机号')

        # 2.2 密码
        if not re.match(r'^[0-9A-Za-z]{8,20}$', password):
            return HttpResponseForbidden('密码错误')

        if not re.match(r'^[A-Za-z]*$', remembered):
            return HttpResponseForbidden('状态错误')

        # 3.验证用户名和密码--django自带的认证
        user = authenticate(username=username, password=password)

        if user is None:
            return render(request, 'login.html', {'account_errmsg': '用户名或密码错误'})

        # 4.保持登录状态
        login(request, user)

        # 5.是否记住用户名
        if remembered != 'on':
            # 不记住用户名, 浏览器结束会话就过期
            request.session.set_expiry(0)
        else:
            # 记住用户名, 浏览器会话保持两周
            request.session.set_expiry(None)

        # 6.返回响应结果
        next_page = request.GET.get('next')
        if next_page:
            response = redirect(next_page)
        else:
            response = redirect(reverse('contents:index'))
        # 注册时用户名写入到cookie，有效期15天
        response.set_cookie('username', user.username, max_age=15 * 3600 * 24)
        merge_cart_cookie_to_redis(request=request, response=response)
        return response


# 1.用户注册功能
class RegisterView(View):
    # 注册用户

    def get(self, request):
        return render(request, 'register.html')

    def post(self, request):
        # 用户注册功能
        # return 错误说明，或者注册成功后跳转到主页
        username = request.POST.get('username')
        password = request.POST.get('password')
        password2 = request.POST.get('password2')
        mobile = request.POST.get('mobile')
        allow = request.POST.get('allow')
        sms_code = request.POST.get('msg_code')

        if not all([username, password, password2, mobile, allow, sms_code]):
            return http.HttpResponseForbidden('缺少必传参数')

        if not re.match(r'^\w{5,20}$', username):
            return http.HttpResponseForbidden('请输入5-20个字符的用户名,可使用字母、数字、下划线')

        if not re.match(r'^\d{6}$', sms_code):
            return http.HttpResponseForbidden('验证码错误')

        if not re.match(r'^[0-9A-Za-z]{8,20}$', password):
            return http.HttpResponseForbidden('请输入8-20位的密码，可使用数字、字母')
        if not re.match(r'^[0-9A-Za-z]{8,20}$', password2):
            return http.HttpResponseForbidden('请输入8-20位的密码，可使用数字、字母')

        if not re.match(r'^1[3-9]\d{9}$', mobile):
            return http.HttpResponseForbidden('请输入正确的手机号码')

        if password != password2:
            return http.HttpResponseForbidden('两次输入的密码不一致')

        if allow != 'on':
            return http.HttpResponseForbidden('请勾选用户协议')

        try:
            sms_redis_client = get_redis_connection('sms_code')
            try:
                sms_redis_code = sms_redis_client.get("sms_%s" % mobile)
            except DatabaseError:
                return http.HttpResponseForbidden('手机验证码已经过期')

            if sms_code != sms_redis_code.decode():
                # 判断是否存在恶意计算验证码的存在
                if human_or_machine(mobile):
                    return http.HttpResponseForbidden('手机验证码错误超过%s次，请等待%s秒后再试！'
                                                      % (MACHINE_TIMES, MACHINE_TIMES_WAIT))
                else:
                    return http.HttpResponseForbidden('手机验证码错误！')

            # 删除已经正确验证的短信验证码
            try:
                sms_redis_client.delete("sms_%s" % mobile)
            except Exception as e:
                settings.LOGGER.erro(e)
        except DatabaseError:
            return http.HttpResponseForbidden('注册失败')

        try:
            # 保证数据库尽量少的受到打扰，同时保证不会被恶意拉取数据库搜索
            u_count = User.objects.filter(username=username).count()
            if u_count:
                return http.HttpResponseForbidden('已经存在的用户名')
            m_count = User.objects.filter(mobile=mobile).count()
            if m_count:
                return http.HttpResponseForbidden('已经存在的手机号码')
        except DatabaseError:
            return http.HttpResponseForbidden('注册失败')

        try:
            user = User.objects.create_user(username=username, password=password, mobile=mobile)
        except DatabaseError:
            return http.HttpResponseForbidden('注册失败')

        # 实现状态保持
        login(request, user)
        request.session.set_expiry(0)
        # 响应注册结果
        response = redirect(reverse('contents:index'))
        # 注册时用户名写入到cookie，有效期15天
        response.set_cookie('username', user.username, max_age=15 * 3600 * 24)
        merge_cart_cookie_to_redis(request=request, response=response)
        return response


class UsernameCountView(View):
    """判断用户名是否重复注册"""

    def get(self, request, username):
        """
        :param request: 请求对象
        :param username: 用户名
        :return: JSON
        """
        count = User.objects.filter(username=username).count()
        return http.JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'count': count})


class MobileCountView(View):
    # 判断手机号是否唯一
    def get(self, request, mobile):
        """
        :param request: 请求对象
        :param mobile: 手机号
        :return: JSON
        """
        count = User.objects.filter(mobile=mobile).count()
        return http.JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'count': count})
